参考 SRE deep dive into Linux Page Cache ,mm/workingset.c ,io_uring 。
环境准备
安装依赖,下载内核源码,编译、安装 page-types
工具,生成测试数据文件,同步、清空缓存。
1 $ sudo apt install git build-essential golang vmtouch
1 2 $ uname -r6.2.0-36-generic
1 2 3 4 5 6 7 $ mkdir kernel$ cd kernel$ wget https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/snapshot/linux-6.2.tar.gz $ tar -xzf linux-6.2.tar.gz $ cd linux-6.2/tools/vm$ make $ sudo make install
1 $ dd if =/dev/random of=/var/tmp/file1.db count=128 bs=1M
1 $ sync ; echo 3 | sudo tee /proc/sys/vm/drop_caches
基本概念
基本操作
File reads
使用 Python 代码从文件读取 2B 数据,发现实际会读取 16 KB 的数据到缓存,操作系统会预读多个页面。使用 posix_fadvise()
提示内核,文件是随机访问的,此时内核不会使用预读优化。实测使用 mmap
系统调用,将文件映射到进程的虚拟内存,依然读取 2B 数据,此时内核预读 32 页而不是 read
系统调用的 4 页。
1 2 with open ("/var/tmp/file1.db" , "br" ) as f: print (f.read(2 ))
1 2 3 4 5 6 $ strace -s0 python3 ./read_2_bytes.py ... openat(AT_FDCWD, "/var/tmp/file1.db", O_RDONLY|O_CLOEXEC) = 3 ... read(3, ""..., 4096) = 4096 ...
1 2 3 4 5 $ vmtouch /var/tmp/file1.db Files: 1 Directories: 0 Resident Pages: 4/32768 16K/128M 0.0122% Elapsed: 0.001331 seconds
1 2 3 4 5 6 import oswith open ("/var/tmp/file1.db" , "br" ) as f: fd = f.fileno() os.posix_fadvise(fd, 0 , os.fstat(fd).st_size, os.POSIX_FADV_RANDOM) print (f.read(2 ))
1 $ echo 3 | sudo tee /proc/sys/vm/drop_caches && python3 ./read_2_random.py
1 2 3 4 5 $ vmtouch /var/tmp/file1.db Files: 1 Directories: 0 Resident Pages: 1/32768 4K/128M 0.00305% Elapsed: 0.001301 seconds
File writes
更新文件的前 2B,发现内核读取 1 页数据到缓存。如果及时查看当前 cgroup 的脏页大小,会发现有 4KB 的数据还未刷盘。也可以使用 cat /proc/meminfo | grep Dirty
查看整个系统中的脏页大小,但是很难利用该信息。
1 2 with open("/var/tmp/file1.db", "br+") as f: print(f.write(b"ab"))
1 $ sync ; echo 3 | sudo tee /proc/sys/vm/drop_caches && python3 ./write_2_bytes.py
1 2 3 4 5 $ vmtouch /var/tmp/file1.db Files: 1 Directories: 0 Resident Pages: 1/32768 4K/128M 0.00305% Elapsed: 0.001764 seconds
1 2 3 4 $ cat /proc/self/cgroup0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.gnome.Terminal.slice/vte-spawn-7fe5140d-9b95-4aff-9979-de88b1c42b94.scope $ grep dirty /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/app-org.gnome.Terminal.slice/vte-spawn-7fe5140d-9b95-4aff-9979-de88b1c42b94.scope/memory.stat file_dirty 4096
1 2 3 4 5 6 7 8 $ sudo page-types -f /var/tmp/file1.db -b dirty /var/tmp/file1.db Inode: 402310 Size: 134217728 (32768 pages) Modify: Mon Oct 13 19:35:25 2025 (2 seconds ago) Access: Mon Oct 13 18:21:11 2025 (4456 seconds ago) flags page-count MB symbolic-flags long-symbolic-flags 0x0000000000000038 1 0 ___UDl_______________________________________ uptodate,dirty,lru total 1 0
缓存淘汰
每个 cgroup 都有一对活跃和非活跃列表,一对用于匿名页面,另一对用于文件页面。简单来说,发生缺页的页面被插入到非活跃列表,在非活跃列表被多次访问的页面升级到活跃列表。使用 LRU 算法结合 referenced 标志,控制页面的升级、降级和淘汰。referenced 被置位的页面,相当于在降级/淘汰时可以复活到当前列表的头部。此外,页缓存还会利用 shadow entry 计算 refault distance ,从而减少非活跃列表空间不足导致的内存抖动问题。如果系统有 NUMA 节点,则会为每个节点维护列表,以减少锁竞争。